#ifndef AER_FRUSTUM_HPP_
#define AER_FRUSTUM_HPP_

#include "../aerDefs.hpp"
#include <glm/gtc/matrix_transform.hpp> // for perspective
#include <glm/gtc/matrix_access.hpp>    // get matrix row


namespace aer {

/// + ~ + ~ ~ + ~ ~ + ~ ~ +
///
/// Describes a frustum
///
/// + ~ + ~ ~ + ~ ~ + ~ ~ +
class Frustum
{
  public:
    enum PlaneSide
    {
      PLANE_LEFT,
      PLANE_RIGHT,
      PLANE_TOP,
      PLANE_BOTTOM,      
      PLANE_NEAR,
      PLANE_FAR,
      
      kNumPlane
    };
    
    
  private:
    float m_fov;
    float m_aspectRatio;
    float m_zNear;
    float m_zFar;
    
    Matrix4x4 m_projectionMatrix;
    Matrix4x4 m_invProjectionMatrix;
    bool m_bRebuild;
    bool m_bRebuildInverse;
    
    Vector2 m_linearizationParams;
    
    
  public:
    static 
    void ExtractPlanes(const glm::mat4& matrix, bool bNormalize, glm::vec4 planes[kNumPlane])
    {
      /// if matrix is the projection matrix, clipping plane are in view space
      /// if matrix is the MVP matrix, clipping plane are in model space
      
      const glm::mat4& M = glm::transpose(matrix);
      
      planes[PLANE_LEFT]   =  M[3] + M[0]; // Left
      planes[PLANE_RIGHT]  =  M[3] - M[0]; // Right
      planes[PLANE_BOTTOM] =  M[3] + M[1]; // Bottom
      planes[PLANE_TOP]    =  M[3] - M[1]; // Top      
      planes[PLANE_NEAR]   =  M[3] + M[2]; // Near
      planes[PLANE_FAR]    =  M[3] - M[2]; // Far
      
      if (bNormalize)
      {
        for (unsigned int i=0u; i<kNumPlane; ++i) {
          planes[i] = glm::normalize( planes[i] );//
        }
      }
    }
    
    
  public:
    Frustum()
      : m_fov(60.0f),
        m_aspectRatio(1.0f),
        m_zNear(0.1f),
        m_zFar(1000.0f),
        m_bRebuild(true),
        m_bRebuildInverse(true)
    {}
    
    Frustum(float fov, float aspectRatio, float zNear, float zFar) 
      : m_fov(fov), 
        m_aspectRatio(aspectRatio), 
        m_zNear(zNear), 
        m_zFar(zFar),
        m_bRebuild(true), 
        m_bRebuildInverse(true)
    {}
    
    
    /// ===========
    /// + Setters +
    /// ===========   
    void set(float fov, float aspectRatio, float zNear, float zFar)
    {
      m_fov         = fov;
      m_aspectRatio = aspectRatio;
      m_zNear       = zNear;
      m_zFar        = zFar;
      m_bRebuild    = true;
      m_bRebuildInverse = true;
    }
    
    void setFOV(float fov) 
    { 
      m_fov = fov; 
      m_bRebuild = m_bRebuildInverse = true;
    }
    
    void setAspectRatio(float aspectRatio) 
    { 
      m_aspectRatio = aspectRatio;  
      m_bRebuild = m_bRebuildInverse = true;
    }
    
    void setZNear(float zNear) 
    { 
      m_zNear = zNear; 
      m_bRebuild = m_bRebuildInverse = true;
    }
    
    void setZFar(float zFar) 
    { 
      m_zFar = zFar;  
      m_bRebuild = m_bRebuildInverse = true;
    }
    
     
    /// ===========
    /// + Getters +
    /// ===========
    const float getFOV()         const { return m_fov; }
    const float getAspectRatio() const { return m_aspectRatio; }
    const float getZNear()       const { return m_zNear; }
    const float getZFar()        const { return m_zFar; }
    
    const Matrix4x4& getProjectionMatrix()
    {
      build();
      return m_projectionMatrix;
    }
    
    const glm::mat4& getInvProjectionMatrix()
    {
      buildInverse();
      return m_invProjectionMatrix; 
    }
    
    const Vector2& getLinearizationParams() const { return m_linearizationParams; }

    bool isDirty() const { return m_bRebuild; }
    
    
  private:
    void build()
    {
      if (!isDirty()) return;
      
      m_projectionMatrix = glm::perspective( m_fov, m_aspectRatio, m_zNear, m_zFar);
      updateLinearizationParams();
      
      m_bRebuild        = false;
      m_bRebuildInverse = true;
    }
    
    void buildInverse()
    {
      if (!m_bRebuildInverse) return;
      
      build();
      m_invProjectionMatrix = glm::inverse( m_projectionMatrix );
      m_bRebuildInverse = false;
    }
    
    void updateLinearizationParams()
    {
      m_linearizationParams.x =  m_zFar / (m_zFar - m_zNear);
      m_linearizationParams.y = -m_zNear * m_linearizationParams.x;
    }
};

}; // aer

#endif // AER_FRUSTUM_HPP_
